Skip to content

feat(output): add basic opentelemetry output#971

Draft
Molter73 wants to merge 2 commits into
mainfrom
mauro/feat/otel-output
Draft

feat(output): add basic opentelemetry output#971
Molter73 wants to merge 2 commits into
mainfrom
mauro/feat/otel-output

Conversation

@Molter73

@Molter73 Molter73 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Description

This output allows exporting file activity events as opentelemetry logs via otlp to be collected by compatible systems.

Checklist

  • Patch has a change log entry OR does not need one.
  • Investigated and inspected CI test results
  • Updated documentation accordingly

Automated testing

  • Added unit tests
  • Added integration tests
  • Added regression tests

If any of these don't apply, please comment below.

Testing Performed

Tested pushing events to Loki and visualizing events in Grafana with the provided documentation.

Summary by CodeRabbit

  • New Features

    • Added optional OpenTelemetry support for collecting and exporting application events.
    • Added configuration via YAML, environment variables, and CLI for setting an OTEL endpoint.
    • Enabled OTEL output alongside existing outputs, with live config reload support.
    • Added tracking metrics for OTEL event delivery and drops.
  • Bug Fixes

    • Improved configuration parsing and validation for OTEL settings.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: 2d650ace-b581-4488-8ef1-1446e282524e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds an optional OpenTelemetry (OTLP) logging output to fact, gated behind an otel Cargo feature, with configuration, event serialization, and a refactored subscriber-based output pipeline. It also adds otel documentation/Grafana examples, migrates integration tests to support gRPC/OTLP dual-mode testing, and refactors CI workflows into a reusable container-build workflow with new otel-aware image targets.

Changes

OpenTelemetry Output Feature

Layer / File(s) Summary
OTelConfig, CLI, and reload wiring
fact/Cargo.toml, fact/src/config/mod.rs, fact/src/config/reloader.rs, fact/src/config/tests.rs
Adds optional OTel dependencies/feature, OTelConfig with YAML parsing/merging, CLI --otel-endpoint, FactCli::into_config, a Reloader otel() subscription channel, and expanded config tests.
Event/Process AnyValue conversions
fact/src/event/mod.rs, fact/src/event/process.rs
Adds From<...> for AnyValue conversions for Event, FileData variants, Process, and Lineage behind the otel feature.
Subscriber-based output pipeline and OTel client
fact/src/output/mod.rs, fact/src/output/grpc.rs, fact/src/output/stdout.rs, fact/src/output/otel.rs, fact/src/metrics/mod.rs, fact/src/lib.rs
Refactors gRPC/stdout clients to request event receivers via an mpsc/oneshot subscription channel, orchestrates workers with a JoinSet, adds a new OTel Client forwarding events to an OTLP HTTP exporter, and adds an otel events metric.
OTel documentation and Grafana/Loki examples
docs/otel/*
Adds otel.md documentation and example Grafana datasource/dashboard/provider and Loki config files.
Dual-mode test server and fixtures
tests/server.py, tests/conftest.py, tests/event.py, tests/Makefile, tests/requirements.txt
Replaces FileActivityService with an EventServer abstraction plus GrpcServer/OtlpServer, parametrizes tests over output modes, and reworks event diffing to compare model objects.
Test suite EventServer type updates
tests/test_*.py, tests/test_editors/*, tests/utils.py
Updates server fixture annotations from FileActivityService to EventServer across test modules and fixes a quoting edge case in rust_style_quote.

CI/CD and Build Infrastructure

Layer / File(s) Summary
Reusable container-build workflow
.github/workflows/container-build.yml, .github/workflows/ci.yml
Adds a new reusable workflow computing tags, building multi-arch images, and pushing manifests; rewires ci.yml to call it for standard and otel variants.
Integration tests VM/output-type parameterization
.github/workflows/integration-tests.yml, .github/workflows/konflux-tests.yml
Adds vms/output-type inputs, derives test matrix from inputs, injects output type into vars.yml, and updates artifact naming.
otel image build target and pytest wiring
Containerfile, Makefile, ansible/run-tests.yml
Adds CARGO_ARGS build arg, a Makefile image-otel target, and OUTPUT_TYPE passthrough in ansible pytest execution.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EventLoop as output::start
  participant OtelClient as otel::Client
  participant Exporter as OTLP LogExporter
  participant Collector as OTLP/Loki Endpoint

  EventLoop->>OtelClient: subscribe via oneshot handshake
  OtelClient->>OtelClient: receive Event from broadcast
  OtelClient->>OtelClient: convert Event to AnyValue LogRecord
  OtelClient->>Exporter: emit log record
  Exporter->>Collector: POST /v1/logs (protobuf)
Loading
sequenceDiagram
  participant CI as ci.yml
  participant Build as container-build.yml
  participant Registry as Quay Registries

  CI->>Build: workflow_call (tag-suffix, make-target)
  Build->>Build: vars job computes image tags
  Build->>Registry: push stackrox-io image (amd64/arm64)
  Build->>Registry: push rhacs-eng retagged image
  Build->>Registry: create/push multi-arch manifests
  Build-->>CI: output tag
Loading

Suggested reviewers: ovalenti

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding basic OpenTelemetry output.
Description check ✅ Passed The description follows the template and includes a summary, checklist, automated testing, and testing performed.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mauro/feat/otel-output

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.75000% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.95%. Comparing base (ed338ea) to head (4214811).

Files with missing lines Patch % Lines
fact/src/output/mod.rs 0.00% 19 Missing ⚠️
fact/src/config/reloader.rs 0.00% 12 Missing ⚠️
fact/src/event/mod.rs 0.00% 8 Missing ⚠️
fact/src/metrics/mod.rs 0.00% 4 Missing ⚠️
fact/src/output/grpc.rs 0.00% 3 Missing ⚠️
fact/src/config/mod.rs 93.93% 2 Missing ⚠️
fact/src/lib.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #971      +/-   ##
==========================================
+ Coverage   32.64%   32.95%   +0.30%     
==========================================
  Files          21       21              
  Lines        2916     2971      +55     
  Branches     2916     2971      +55     
==========================================
+ Hits          952      979      +27     
- Misses       1961     1989      +28     
  Partials        3        3              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch 5 times, most recently from 229cd7d to 8bb3067 Compare July 6, 2026 14:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/event.py (1)

298-317: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

.match() doesn't anchor at the end of the string.

expected.match(actual) only checks that actual starts with a match for expected; a longer actual value with unexpected trailing characters would still be considered a match. Use fullmatch() for exact validation of regex-based expectations.

🐛 Proposed fix
-            if not isinstance(actual, str) or not expected.match(actual):
+            if not isinstance(actual, str) or not expected.fullmatch(actual):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/event.py` around lines 298 - 317, The regex check in _diff_path
currently uses expected.match(actual), which allows extra trailing characters to
slip through. Update the Pattern branch in _diff_path to use fullmatch() instead
so regex-based expectations must match the entire actual path, while keeping the
existing non-regex equality behavior unchanged.
🧹 Nitpick comments (5)
.github/workflows/container-build.yml (2)

25-28: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider persist-credentials: false on checkout steps.

zizmor's artipacked finding notes these actions/checkout@v4 steps don't set persist-credentials: false, leaving the ambient GITHUB_TOKEN in .git/config for the remainder of the job — including during cargo build/podman build, which execute third-party build scripts.

🔒 Suggested fix
     - uses: actions/checkout@v4
       with:
         submodules: true
         fetch-depth: 0
+        persist-credentials: false

Also applies to: 54-56

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/container-build.yml around lines 25 - 28, The checkout
steps using actions/checkout@v4 currently leave the ambient GITHUB_TOKEN
persisted in .git/config, which can leak into later cargo build and podman build
steps. Update each checkout block in the workflow to set persist-credentials to
false, including the other checkout occurrence referenced by the review, while
keeping the existing submodules and fetch-depth settings in place.

Source: Linters/SAST tools


33-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Hoist interpolated expressions into env: to avoid template injection.

zizmor flags direct ${{ inputs.tag-suffix }}, ${{ inputs.make-target }}, and ${{ needs.vars.outputs.* }} interpolation inside run: blocks (lines 33-35, 59, 98-102, 120-124) as template-injection risk. Even though these values currently originate from static strings in ci.yml, the canonical mitigation is to pass them through a step-level env: block and reference them as shell variables, so the pattern remains safe if these inputs are ever driven by less-trusted values.

🔒 Example fix for the `vars` step
     - id: vars
+      env:
+        TAG_SUFFIX: ${{ inputs.tag-suffix }}
       run: |
         cat << EOF >> "$GITHUB_OUTPUT"
-        tag=$(make tag)${{ inputs.tag-suffix }}
-        stackrox-io-image=$(make image-name)${{ inputs.tag-suffix }}
-        rhacs-eng-image=$(FACT_REGISTRY="quay.io/rhacs-eng/fact" make image-name)${{ inputs.tag-suffix }}
+        tag=$(make tag)${TAG_SUFFIX}
+        stackrox-io-image=$(make image-name)${TAG_SUFFIX}
+        rhacs-eng-image=$(FACT_REGISTRY="quay.io/rhacs-eng/fact" make image-name)${TAG_SUFFIX}
         EOF

Similarly for line 59 (env: MAKE_TARGET: ${{ inputs.make-target }} then run: DOCKER=podman make "${MAKE_TARGET}") and the manifest steps (98-102, 120-124), which interpolate needs.vars.outputs.* directly.

Also applies to: 59-59, 98-102, 120-124

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/container-build.yml around lines 33 - 35, The workflow
steps that build image tags and run make targets are interpolating GitHub
expressions directly inside run blocks, which is the template-injection risk
flagged by zizmor. Move the affected values from inputs.tag-suffix,
inputs.make-target, and needs.vars.outputs.* into step-level env entries, then
reference those shell variables inside the run commands. Apply this pattern in
the tag/image-name step, the MAKE_TARGET step, and the manifest-related steps so
the logic stays the same but the expressions are no longer expanded directly in
shell scripts.

Source: Linters/SAST tools

tests/conftest.py (1)

70-100: 🩺 Stability & Availability | 🔵 Trivial

Fixed OTLP port may conflict under parallel test execution.

OtlpServer.serve() defaults to a hardcoded port (4318), mirroring the existing fixed-port pattern for GrpcServer (9999). If these tests are ever run with pytest-xdist or otherwise in parallel across workers, both fixed ports could collide across concurrently-running test processes. Not a regression introduced by this diff, but doubling the fixed-port surface (grpc + otlp) is worth tracking if parallelization is a goal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 70 - 100, The server fixture currently
hardcodes OTLP to a fixed port via OtlpServer.serve(), which can collide when
tests run in parallel. Update the server setup in pytest_generate_tests/server
so OtlpServer can be started with a worker-unique or free port (similar to how
GrpcServer is selected by mode), and make the fixture pass that port through
instead of relying on the default 4318. Ensure the change is localized around
_get_output_modes, pytest_generate_tests, and server so each test process gets
an isolated OTLP endpoint.
tests/server.py (1)

41-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

ThreadPoolExecutor is never shut down.

Neither GrpcServer.stop() nor OtlpServer.stop() calls self.executor.shutdown(). Each test using the server fixture creates a fresh EventServer subclass instance with its own 2-worker pool that's never explicitly released, leaking idle threads across the test session (they'll only terminate at interpreter exit).

♻️ Proposed fix
     def stop(self):
         """Stop the gRPC server."""
         self.server.stop(1)
         self.running.clear()
+        self.executor.shutdown(wait=False)

and similarly for OtlpServer.stop().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server.py` around lines 41 - 58, `EventServer` creates a
`ThreadPoolExecutor` in its constructor but the pool is never released, so
update the shutdown path in both `GrpcServer.stop()` and `OtlpServer.stop()` to
call `self.executor.shutdown()` after stopping the server work. Make the cleanup
part of the existing `stop` methods so each `EventServer` subclass instance
properly tears down its worker threads when the test fixture finishes.
fact/src/event/process.rs (1)

45-57: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Inconsistent attribute key naming: exec_path vs exe_path.

Lineage's exe_path field is emitted under the key "exec_path", while the sibling Process conversion (line 259-261 in this same file) emits the analogous field under "exe_path". Since these are exported as OTLP log attributes consumed by external tooling (Grafana/Loki dashboards per this PR's docs), inconsistent naming for the same semantic field across nested maps will confuse queries/dashboards built against the OTel schema.

🔧 Suggested fix
 impl From<Lineage> for opentelemetry::logs::AnyValue {
     fn from(value: Lineage) -> Self {
         AnyValue::Map(Box::new(HashMap::from([
             ("uid".into(), value.uid.into()),
             (
-                "exec_path".into(),
+                "exe_path".into(),
                 value.exe_path.to_string_lossy().to_string().into(),
             ),
         ])))
     }
 }

This isn't exercised by the current Python OTLP test server (which doesn't parse lineage), so it wouldn't be caught by CI as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fact/src/event/process.rs` around lines 45 - 57, The `Lineage` OTEL
conversion is exporting the `exe_path` field under a different attribute name
than the sibling `Process` conversion, causing inconsistent log schema. Update
the `From<Lineage> for opentelemetry::logs::AnyValue` implementation in
`process.rs` so the map key matches the existing `Process` export naming
(`exe_path`) and keep the attribute naming consistent across both conversions
for downstream queries and dashboards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@fact/src/output/otel.rs`:
- Around line 65-70: The LogExporter setup in otel initialization is panicking
on build failure via expect, which should be converted to normal error handling.
Update the LogExporter::builder() chain in the otel initialization path to
propagate the ExporterBuildError with ? or a context-wrapped error instead of
expect, so failures flow through the existing Result-based path rather than
crashing.

In `@fact/src/output/stdout.rs`:
- Around line 27-29: The subscription handshake in the stdout output path can
panic if the orchestrator is shutting down and the oneshot/channel is already
closed. In the task that uses std::sync::oneshot::channel and
self.subscriber.send(tx), replace both unwrap() calls with graceful error
handling: if sending the subscription request fails or rx.await returns an
error, log the failure and return early instead of panicking. Use the existing
stdout subscription flow around the send/receive handshake to locate the fix.

In `@tests/utils.py`:
- Around line 61-68: The string-quoting logic in the helper that builds
shell-safe literals has an unsafe fallback in the single-quote path for values
containing backslashes or quotes plus backticks or dollar signs. Update the
quoting branch in the relevant utility function to route those cases through the
double-quoted form instead, and make that branch escape backslash, double quote,
backtick, and dollar-sign characters so the value is always treated literally.
Keep the single-quoted fallback only for strings that do not need those extra
escapes.

---

Outside diff comments:
In `@tests/event.py`:
- Around line 298-317: The regex check in _diff_path currently uses
expected.match(actual), which allows extra trailing characters to slip through.
Update the Pattern branch in _diff_path to use fullmatch() instead so
regex-based expectations must match the entire actual path, while keeping the
existing non-regex equality behavior unchanged.

---

Nitpick comments:
In @.github/workflows/container-build.yml:
- Around line 25-28: The checkout steps using actions/checkout@v4 currently
leave the ambient GITHUB_TOKEN persisted in .git/config, which can leak into
later cargo build and podman build steps. Update each checkout block in the
workflow to set persist-credentials to false, including the other checkout
occurrence referenced by the review, while keeping the existing submodules and
fetch-depth settings in place.
- Around line 33-35: The workflow steps that build image tags and run make
targets are interpolating GitHub expressions directly inside run blocks, which
is the template-injection risk flagged by zizmor. Move the affected values from
inputs.tag-suffix, inputs.make-target, and needs.vars.outputs.* into step-level
env entries, then reference those shell variables inside the run commands. Apply
this pattern in the tag/image-name step, the MAKE_TARGET step, and the
manifest-related steps so the logic stays the same but the expressions are no
longer expanded directly in shell scripts.

In `@fact/src/event/process.rs`:
- Around line 45-57: The `Lineage` OTEL conversion is exporting the `exe_path`
field under a different attribute name than the sibling `Process` conversion,
causing inconsistent log schema. Update the `From<Lineage> for
opentelemetry::logs::AnyValue` implementation in `process.rs` so the map key
matches the existing `Process` export naming (`exe_path`) and keep the attribute
naming consistent across both conversions for downstream queries and dashboards.

In `@tests/conftest.py`:
- Around line 70-100: The server fixture currently hardcodes OTLP to a fixed
port via OtlpServer.serve(), which can collide when tests run in parallel.
Update the server setup in pytest_generate_tests/server so OtlpServer can be
started with a worker-unique or free port (similar to how GrpcServer is selected
by mode), and make the fixture pass that port through instead of relying on the
default 4318. Ensure the change is localized around _get_output_modes,
pytest_generate_tests, and server so each test process gets an isolated OTLP
endpoint.

In `@tests/server.py`:
- Around line 41-58: `EventServer` creates a `ThreadPoolExecutor` in its
constructor but the pool is never released, so update the shutdown path in both
`GrpcServer.stop()` and `OtlpServer.stop()` to call `self.executor.shutdown()`
after stopping the server work. Make the cleanup part of the existing `stop`
methods so each `EventServer` subclass instance properly tears down its worker
threads when the test fixture finishes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: 6070caf3-6f12-4fe2-99cd-d0c9f8b532da

📥 Commits

Reviewing files that changed from the base of the PR and between e4718f9 and cda03b6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • .github/workflows/ci.yml
  • .github/workflows/container-build.yml
  • .github/workflows/integration-tests.yml
  • .github/workflows/konflux-tests.yml
  • Containerfile
  • Makefile
  • ansible/run-tests.yml
  • docs/otel/grafana-dashboard-provider.yaml
  • docs/otel/grafana-datasource.yaml
  • docs/otel/grafana-fact-dashboard.json
  • docs/otel/loki-config.yaml
  • docs/otel/otel.md
  • fact/Cargo.toml
  • fact/src/config/mod.rs
  • fact/src/config/reloader.rs
  • fact/src/config/tests.rs
  • fact/src/event/mod.rs
  • fact/src/event/process.rs
  • fact/src/lib.rs
  • fact/src/metrics/mod.rs
  • fact/src/output/grpc.rs
  • fact/src/output/mod.rs
  • fact/src/output/otel.rs
  • fact/src/output/stdout.rs
  • tests/Makefile
  • tests/conftest.py
  • tests/event.py
  • tests/requirements.txt
  • tests/server.py
  • tests/test_config_hotreload.py
  • tests/test_editors/test_nvim.py
  • tests/test_editors/test_sed.py
  • tests/test_editors/test_vi.py
  • tests/test_editors/test_vim.py
  • tests/test_file_open.py
  • tests/test_misc.py
  • tests/test_path_chmod.py
  • tests/test_path_chown.py
  • tests/test_path_mkdir.py
  • tests/test_path_rename.py
  • tests/test_path_rmdir.py
  • tests/test_path_unlink.py
  • tests/test_rate_limit.py
  • tests/test_wildcard.py
  • tests/test_xattr.py
  • tests/utils.py

Comment thread fact/src/output/otel.rs Outdated
Comment thread fact/src/output/stdout.rs Outdated
Comment on lines +27 to +29
let (tx, rx) = oneshot::channel();
self.subscriber.send(tx).await.unwrap();
let mut rx = rx.await.unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle subscription handshake failure gracefully instead of unwrap().

If the output orchestrator's loop has already exited (e.g. during shutdown), subs_rx/the oneshot sender may be dropped, making these .unwrap()s panic in the spawned task. Prefer logging and returning, matching the graceful termination used elsewhere.

🛡️ Proposed fix
-            let (tx, rx) = oneshot::channel();
-            self.subscriber.send(tx).await.unwrap();
-            let mut rx = rx.await.unwrap();
+            let (tx, rx) = oneshot::channel();
+            if self.subscriber.send(tx).await.is_err() {
+                info!("Output orchestrator gone, stopping stdout output...");
+                return;
+            }
+            let mut rx = match rx.await {
+                Ok(rx) => rx,
+                Err(_) => {
+                    info!("Failed to obtain event receiver, stopping stdout output...");
+                    return;
+                }
+            };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let (tx, rx) = oneshot::channel();
self.subscriber.send(tx).await.unwrap();
let mut rx = rx.await.unwrap();
let (tx, rx) = oneshot::channel();
if self.subscriber.send(tx).await.is_err() {
info!("Output orchestrator gone, stopping stdout output...");
return;
}
let mut rx = match rx.await {
Ok(rx) => rx,
Err(_) => {
info!("Failed to obtain event receiver, stopping stdout output...");
return;
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fact/src/output/stdout.rs` around lines 27 - 29, The subscription handshake
in the stdout output path can panic if the orchestrator is shutting down and the
oneshot/channel is already closed. In the task that uses
std::sync::oneshot::channel and self.subscriber.send(tx), replace both unwrap()
calls with graceful error handling: if sending the subscription request fails or
rx.await returns an error, log the failure and return early instead of
panicking. Use the existing stdout subscription flow around the send/receive
handshake to locate the fix.

Comment thread tests/utils.py Outdated
Comment on lines 61 to 68
if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
# Backslash and single-quote cannot appear in single-quoted
# strings in Rust's shlex, use double-quoting instead.
if ('\\' in s or "'" in s) and '`' not in s and '$' not in s:
escaped = s.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
escaped = s.replace("'", "\\'")
return f"'{escaped}'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Broken single-quote fallback for strings with both a quote and backtick/$.

When a string contains a backslash or ' and also contains ` or $, the code falls through to the single-quote branch (s.replace("'", "\\'")). This contradicts the function's own comment: backslash has no escaping power inside single-quoted shell strings, so \' does not produce a literal quote — it just embeds a literal backslash and then the string is prematurely terminated by the unescaped '. The rest of the string (containing the backtick/$) ends up unquoted, exposing it to command substitution/expansion instead of being treated literally.

The double-quote branch already handles \ and "; extending it to also escape ` and $ removes the need for this unsafe fallback entirely.

🐛 Proposed fix
     if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
-        # Backslash and single-quote cannot appear in single-quoted
-        # strings in Rust's shlex, use double-quoting instead.
-        if ('\\' in s or "'" in s) and '`' not in s and '$' not in s:
-            escaped = s.replace('\\', '\\\\').replace('"', '\\"')
+        # Backslash and single-quote cannot appear in single-quoted
+        # strings in Rust's shlex, use double-quoting instead.
+        if '\\' in s or "'" in s:
+            escaped = (
+                s.replace('\\', '\\\\')
+                .replace('"', '\\"')
+                .replace('`', '\\`')
+                .replace('$', '\\$')
+            )
             return f'"{escaped}"'
         escaped = s.replace("'", "\\'")
         return f"'{escaped}'"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
# Backslash and single-quote cannot appear in single-quoted
# strings in Rust's shlex, use double-quoting instead.
if ('\\' in s or "'" in s) and '`' not in s and '$' not in s:
escaped = s.replace('\\', '\\\\').replace('"', '\\"')
return f'"{escaped}"'
escaped = s.replace("'", "\\'")
return f"'{escaped}'"
if re.search(r'[^a-zA-Z0-9_.:\-/+@\]]', s):
# Backslash and single-quote cannot appear in single-quoted
# strings in Rust's shlex, use double-quoting instead.
if '\\' in s or "'" in s:
escaped = (
s.replace('\\', '\\\\')
.replace('"', '\\"')
.replace('`', '\\`')
.replace('$', '\\$')
)
return f'"{escaped}"'
escaped = s.replace("'", "\\'")
return f"'{escaped}'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils.py` around lines 61 - 68, The string-quoting logic in the helper
that builds shell-safe literals has an unsafe fallback in the single-quote path
for values containing backslashes or quotes plus backticks or dollar signs.
Update the quoting branch in the relevant utility function to route those cases
through the double-quoted form instead, and make that branch escape backslash,
double quote, backtick, and dollar-sign characters so the value is always
treated literally. Keep the single-quoted fallback only for strings that do not
need those extra escapes.

@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch from f4dda9f to a52c44e Compare July 7, 2026 15:55
@Molter73 Molter73 changed the base branch from main to mauro/feat/resubscription-change July 7, 2026 15:55
@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch from a52c44e to b915ce8 Compare July 7, 2026 16:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
fact/src/output/grpc.rs (1)

207-259: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep the broadcast receiver alive across reconnects. Re-subscribing inside the reconnect loop creates a gap: events published while the gRPC stream is down are never delivered to this client, and Lagged only covers backlog on an already-subscribed receiver. If that loss is intentional, add a metric for the gap; otherwise reuse the receiver across reconnects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fact/src/output/grpc.rs` around lines 207 - 259, The reconnect logic in run
currently re-subscribes on every loop iteration, which drops any events
published while the gRPC stream is down. Update the FileActivityServiceClient
subscription flow in run so the broadcast receiver is created once and kept
alive across reconnects instead of calling self.subscriber.send(tx) each retry;
if intentional loss is expected, add a metric for the reconnect gap.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@fact/src/output/grpc.rs`:
- Around line 207-259: The reconnect logic in run currently re-subscribes on
every loop iteration, which drops any events published while the gRPC stream is
down. Update the FileActivityServiceClient subscription flow in run so the
broadcast receiver is created once and kept alive across reconnects instead of
calling self.subscriber.send(tx) each retry; if intentional loss is expected,
add a metric for the reconnect gap.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: a43a125f-8af4-4b1a-b4ff-05bf76a151bd

📥 Commits

Reviewing files that changed from the base of the PR and between cda03b6 and b915ce8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • CHANGELOG.md
  • fact/Cargo.toml
  • fact/src/config/mod.rs
  • fact/src/config/reloader.rs
  • fact/src/config/tests.rs
  • fact/src/event/mod.rs
  • fact/src/event/process.rs
  • fact/src/lib.rs
  • fact/src/metrics/mod.rs
  • fact/src/output/grpc.rs
  • fact/src/output/mod.rs
  • fact/src/output/otel.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • fact/src/lib.rs
  • fact/src/metrics/mod.rs
  • fact/src/config/tests.rs
  • fact/src/config/mod.rs
  • fact/src/output/otel.rs
  • fact/src/output/mod.rs
  • fact/Cargo.toml
  • fact/src/event/mod.rs
  • fact/src/event/process.rs
  • fact/src/config/reloader.rs

Base automatically changed from mauro/feat/resubscription-change to main July 8, 2026 11:26
Molter73 added 2 commits July 8, 2026 15:12
This output allows exporting file activity events as opentelemetry logs
via otlp to be collected by compatible systems.

The output is gated behind the "otel" feature, which allows regular fact
builds to keep using just the gRPC and stodout outputs, no additional
dependencies.

WIP
@Molter73 Molter73 force-pushed the mauro/feat/otel-output branch from b915ce8 to 4214811 Compare July 8, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants